Skip to content

🔒 全面加固跨世界 GM RPC、用户脚本、Sandbox 与 Agent 边界 - #1747

Open
cyfung1031 wants to merge 248 commits into
scriptscat:mainfrom
cyfung1031:codex/main-world-security-refactor
Open

cyfung1031 wants to merge 248 commits into
scriptscat:mainfrom
cyfung1031:codex/main-world-security-refactor

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

本 PR 的说明仅以当前完整代码树为准:scriptscat:main 80854540dd1759e55ce79f541fcf582ba9c0a499 对比 PR head cc195781d048b7961e6af7d14029ce739998c73c。以下内容描述这两个最终状态之间的净差异,不引用 PR 中间 commits 曾经采用、随后修改或回退的方案。

相较 main,本 PR 主要加固 MAIN world、USER_SCRIPT、Service Worker、Offscreen/Sandbox 与 Agent/CAT API 的跨上下文身份、传输、数据和生命周期边界;同时补齐 early-start 同步状态、GM storage 持久化兼容语义,以及异步 GM/RPC 收尾行为。

变更总览

范围 HEAD vs main 的净变化 作用
MAIN world 通道 MAIN ↔ scripting 使用新增的 PageEventMessage,以每个 document 的随机 eventFlag 派生 performance CustomEvent 名称;pageLoad、value/event、external API 与 GM RPC 共用该 keyed bridge 避免把完整 MAIN traffic 放在任意页面脚本都能无条件订阅的全局 window.message 总线上;随机 key 只降低被动可观察性,不承担授权
页面 GM RPC 身份 新增 PageRpcRegistry 与 Service Worker execution binding;Page RPC v2 wire 使用 handle + sequence + api + params,canonical UUID/run flag/envTag/grant 由 Service Worker 根据 handle 与真实 sender 解析 页面不能用自报 UUID、run flag、envTag 或 grant 决定身份/权限;handle 只作为已签发 binding 的索引
Page RPC 重放 新增固定大小 RequestSequenceWindow;重复序列和窗口外旧序列拒绝,合法的大幅前向 gap 可按固定成本推进窗口 在防重放的同时避免无界 requestId 集合;broker-only sequence gap 也不会永久卡死长期 binding
USER_SCRIPT 传输 content 的普通 GM RPC、bootstrap、value update 与 event callback 使用浏览器原生 ExtensionMessage / user-script connection;同步 DOM 节点辅助仍单独使用 CustomEvent 路径;专用 user-script listener 不可用时可走 document token 绑定的 extension-port compatibility path 将普通特权 traffic 从 page-observable DOM bridge 分离,同时保留节点引用和浏览器兼容所需的窄通道
USER_SCRIPT 生命周期 新增 document/frame 定向的 connection/session、reconnect token、断线后的 pending value update 合并,以及 binding/session 撤销清理 防止旧文档 connection 残留、重连期间 value update 丢失和 callback 错投
Offscreen / Sandbox main 的 Offscreen/EventPage ↔ Sandbox WindowMessage 数据通道改为一次性 Window bootstrap + private MessagePort;parent 校验 exact iframe source、固定 marker 与 exactly one transferred port,随后移除 Window listener;port attachment 同时作为 readiness background/crontab userscript 即使监听 window.message,也不能被动看到后续 sandbox 内部 GM/lifecycle/value/event/skill payload
Sandbox same-realm hardening private-port transport 捕获 MessagePort 方法和原生 MessageEvent.data getter,wire envelope 继续做 data-property/shape 校验 避免 background userscript 后续篡改同 realm DOM prototype 后观察或干扰 trusted transport 的读取/发送
Wrapper provenance normal/early wrapper 使用统一 canonical wrapper source,closure 绑定 build token、创建时 Document、metadata,并记录 UUID/flag/scriptRevision;执行前用捕获的原生 Function.prototype.toString 和当前 Document 读取 metadata,再与 authoritative script revision 对齐 页面替换的函数、其他 Document 的 wrapper、旧 compiled revision 都不能直接取得当前 ScriptInfo/权限继续执行
Trusted runtime state trusted ScriptInfo / GM state 更新使用 own data-property 安装与安全刷新,不依赖继承 setter 或 Object.assign 的普通赋值语义 避免 Object.prototype setter、__proto__ 等页面可控原型行为影响内部权威状态更新
Early-start preload main 会把 early-start scriptInfo 放入 page-visible bootstrap event;HEAD 的 event detail 只保留 scriptFlag,同步 value/config/userConfig/resource snapshot 保留在 registered wrapper closure;execution binding 仍由 authoritative pageLoad 下发 保持 document-start 首行同步 GM 读取,同时减少 page-visible bootstrap payload,并把同步 snapshot 与 document-bound privilege 分离
Early-start reconcile authoritative pageLoad 与 bridge negotiation 并行;early context reconcile 校验 UUID/flag/scriptRevision/execution binding,并保留 bootstrap 前脚本自身已完成的 set/delete key 后到 pageLoad 不会把 early read-modify-write 回滚,也不会让 stale wrapper 继承当前文档的新权限
Early snapshot freshness 对需要同步 value-read 的 enabled early-start script,按 storageName 串行 DB commit、value delivery 与 registered snapshot refresh;shared storage 同批更新;value-only refresh 不推进 static code/resource revision,并有 dirty/repair 路径 下一次 document-start 的 wrapper snapshot 与已提交 storage 保持同 generation,避免并发 write/registration update 发生 generation 倒退
GM storage serialization 新增明确的 userscript storage normalization:array 保持数组形状;非 array object 按 own enumerable string properties 序列化;object 内 Function/Symbol/undefined 省略、array 对应项归一为 null;cycle 无效;top-level Function/Symbol/BigInt 继续使用 ScriptCat 的删除语义;getter/Proxy property read 在此 compatibility surface 可观察 将 GM storage 的兼容 serialization 与 privileged DTO 的 strict clone 分开,统一 immediate、RPC-settled 与 reload-persisted 结果
GM value wire / 字典形状 REncoded 增加 NaN、Infinity、-Infinity 显式 tag;all-values 路径返回 null-prototype dictionary,并安全保留 __proto__ 等 own key 非有限 number 可无损往返;值字典不受 prototype key 污染
Privileged DTO / message boundary Page RPC、ExtensionMessage、WindowMessage、CustomEvent、Server action、USER_SCRIPT 与 Sandbox port envelope 增加严格字段、descriptor、版本/来源与 clone 校验;Map/Set/普通对象条目在 privileged clone 前检查;关键原生方法提前捕获 hostile accessor、Proxy、Symbol、函数、不可 clone 值或被篡改内建不会直接进入 privileged service
GM facade / grant policy GM broker、transport、CAT conversation state 和受保护字段进一步收进 private/facade state;Page RPC effective grant、none grant、别名和依赖图统一使用同一 policy 页面脚本拿不到完整内部 broker 状态;不同 Page RPC consumer 不会各自解释 grant
GM XHR 生命周期 setup/connect/abort/error/timeout/load/loadend 统一 settle-once;用户 callback 抛错不会阻断内部收尾;连接建立前 abort、setup/connect failure 与缺失 broker loadend 都有明确完成路径 Promise/回调不会因错误路径悬挂,abort/error 后 connection 和引用可以收尾
GM async value write Promise-based GM.setValue / batch/delete 以 Service Worker RPC 与 freshness barrier 完成为准,不再依赖 valueUpdate broadcast 来 resolve;legacy 同步 API 仍立即返回 delivery channel 丢失不会让 Promise 永久等待;持久化完成和广播传播职责分开
Connected RPC Server connection 记录 caller disconnect;handler 在 caller 已断开后完成或 reject 时,不再向失效 connection 回包 消除 late response 对关闭端口的无效发送;业务 handler cancellation 仍由各服务自行决定
Agent/CAT canonical identity userscript Agent GM handlers 以 request.script.uuid 覆盖/注入 script identity;conversation/task/attachment/OPFS/DOM monitor 的访问检查使用该 canonical identity 调用脚本不能靠 request payload 中自报 scriptUuid 访问另一脚本资源
Agent ownership / concurrency script conversation 写入 ownerScriptUuid 并按 owner 检查;task CRUD/run/history 按 owner scope;task 使用 generation/revision;attachment read 需在调用脚本拥有的持久化 conversation 中可达 防止跨脚本读取/修改持久化 Agent 资源,并减少 stale update 覆盖
DOM/CDP monitor monitor session 记录 owner;peek/stop/start 检查 owner;同一 tab 的 monitor transition 通过串行队列执行 防止跨脚本读取/替换 monitor,以及并发 attach/detach 留下 debugger/listener
运行时内建与热路径 编译执行改用捕获的 call primitive;关键 Map/Set/Reflect/descriptor 操作使用捕获原生;null-prototype record 在安全前提下直接赋值;共享 message envelope / WindowMessageConnect 减少重复实现 降低页面原型篡改影响,并减少不必要的 bind/descriptor/adapter 开销
回归测试 新增/扩展 MAIN keyed bridge、Sandbox private port、Page RPC、wrapper revision、USER_SCRIPT reconnect、early snapshot、GM storage persistence、GM XHR、RPC disconnect、Agent owner/OPFS/DOM monitor 等 unit/E2E 把上述 HEAD vs main 的边界和兼容语义固定为可重复验证

PR 范围

项目 当前值
比较基准 scriptscat:main @ 80854540dd1759e55ce79f541fcf582ba9c0a499
PR Head codex/main-world-security-refactor @ cc195781d048b7961e6af7d14029ce739998c73c
Ahead / behind 239 / 0 commits
Changed files 143
Diff +16,266 / -2,000

实现考虑

设计约束 当前 HEAD 的 invariant
句柄不是秘密 execution handle 是 Service Worker 签发 binding 的索引;最终授权仍由真实 sender 的 tab/frame/document、脚本状态、grant 与 permission verification 决定
随机 event key 不是认证 MAIN keyed event 只减少普通页面的被动全量可观察性;一旦页面获知 event key,该输入面仍是 page-visible,因此所有 privileged consumer 仍需独立校验
MessagePort 是传输隔离能力 Sandbox private port 将内部 payload 移出共享 Window bus,但不是最终 GM authorization;脚本 context/grant/SW 校验继续成立
Wrapper artifact / instance 分离 UUID/flag/scriptRevision 表示 compiled artifact;closure captured Document 与 SW execution binding 表示具体运行实例/transport identity,三者不互相替代
GM storage 与 privileged DTO 分层 storage compatibility 可以观察调用脚本自己的 enumerable getter/Proxy trap;Page RPC、bridge DTO 与内部状态仍使用 descriptor-first strict validation/clone
Snapshot 是派生状态 early registered snapshot 用于同步 API compatibility,不是授权 secret;storage commit 成功后,registration maintenance failure 只进入 dirty/repair,不反向撤销 DB commit
Async completion 与传播分离 Promise GM write 等待 RPC/freshness barrier;valueUpdate channel 负责传播。RPC 完成不保证所有观察 context 已在同一时刻收到广播
BFCache / navigation same-document/BFCache 可继续使用当前 document 的有效 context;新 document 必须重新取得匹配的 binding/session,导航、脚本撤销和 tab 关闭会清理旧状态
Agent owner scope userscript service 入口以 canonical request.script.uuid 决定 owner;UI/extension-internal 调用与 script-scoped 调用继续走各自现有权限模型
DOM monitor 生命周期 同一 tab 的 start/stop 串行;owner 不匹配的 script caller 不能 peek/stop/replace 现有 monitor

已知限制

  • PR 仍为 Draft,尚未有人类审查;对应 checklist 保持未勾选。
  • MAIN PageEventMessage 仍是 page-visible transport。随机 eventFlag 不是 bearer secret,也不能替代 broker/Service Worker authorization。
  • PageEventMessage 没有通用 ready buffer;当前入口在 eventFlag negotiation 前预取 authoritative pageLoad,但只在 bridge 建立后消费并发送页面 payload。未来新增更早发送的 caller 时仍需单独证明 ordering。
  • Sandbox channel 采用 fail-closed readiness:若 iframe 没有 transfer 合法 MessagePort,parent 不会报告 verified readiness,也不会 replay background/scheduled script;当前没有 timeout-ready 路径。
  • early-start registered snapshot 是派生状态;极端 chrome.userScripts.update/registration failure 下,storage 已成功但下一次 wrapper snapshot 可暂时 dirty,后续 repair/registration mutation 会再尝试恢复。
  • GM storage 是 serialization compatibility surface,不保证 graph identity、prototype、cycle 或大多数 special-object internal slot;ScriptCat top-level unsupported value 继续使用既有 delete 语义,和其他 manager 的瞬时 own-undefined 表现可不同。
  • Connected RPC 的 disconnect 只禁止 late response,不自动取消已经进入业务 handler 的工作;需要真正 cancellation 的服务仍需自己的 AbortSignal/取消协议。
  • Firefox 未完成本轮手动浏览器验证;当前新增浏览器 E2E 主要覆盖 Chromium 路径。

建议审查重点

审查面 重点
MAIN / Page RPC keyed PageEventMessage 上所有 privileged request 是否都经过 shape/clone、handle、grant、sequence 与真实 sender 校验
USER_SCRIPT native connection、bootstrap/reconnect token、pending value delivery 与 DOM-only CustomEvent path 是否严格分离;新 document 是否会清除旧 session/binding
Sandbox Window 只是否用于 one-shot port transfer;exact source/marker/port count、listener removal 与 same-realm prototype hardening 是否完整
Wrapper / early-start canonical source、build token、Document identity、UUID/flag/scriptRevision、authoritative reconcile 和 bootstrap 前本地写入是否组成一致生命周期
GM storage compatibility serialization 与 privileged strict clone 是否保持两个边界;unsupported value、special object、non-finite number 与 prototype key 是否符合测试固定的语义
生命周期 GM XHR、connected RPC、USER_SCRIPT reconnect、snapshot refresh、Sandbox readiness 与 CDP monitor 在 error/abort/disconnect/retry 下是否都能收尾
Agent/CAT userscript request 中自报 identity 是否都被 canonical caller identity 覆盖;conversation/task/attachment/OPFS/monitor 的 owner scope 是否没有漏口
性能 / 可维护性 captured-native 与 null-prototype fast path 是否只用于已证明安全的内部对象;shared message primitives 是否没有扩大 trust boundary
兼容性 document-start 首行同步 GM state、BFCache/navigation、USER_SCRIPT fallback、GM storage immediate/settled/reload 三阶段,以及 Chrome/Firefox 差异

验证

检查 结果
最终 compare scriptscat:main 80854540dd1759e55ce79f541fcf582ba9c0a499 → PR head cc195781d048b7961e6af7d14029ce739998c73c;ahead 239 / behind 0;143 files;+16,266 / -2,000
GitHub Actions 当前 head 的 run 36573773382 为 success;Lint、2 个 unit test shard、4 个 E2E shard 与聚合测试全部 success
MAIN bridge e2e/main-world-keyed-bridge.spec.ts、page_event_message.test.ts、page_rpc.test.ts 覆盖 keyed bridge、message envelope 与 handle/grant/sequence policy
Sandbox e2e/sandbox-message-port.spec.ts、sandbox_message_channel.test.ts、message_port_message.ts 相关测试覆盖 source/marker/one-port bootstrap、listener removal、private traffic 与 prototype poisoning
USER_SCRIPT connection/runtime 单元测试覆盖 native bootstrap/reconnect、document/frame 约束、pending value update 与 callback delivery
Wrapper / early-start executor/runtime/utils 测试覆盖 canonical wrapper metadata、Document/revision mismatch、首行同步 snapshot、authoritative reconcile、bootstrap 前 write/delete preservation 与 snapshot refresh/repair
GM storage example/tests/gm_storage_test.js 与 e2e/gm-api.spec.ts 覆盖 clone/normalization、unsupported value、special object、non-finite number,以及 immediate / RPC-settled / reload-persisted 三阶段
GM XHR / RPC GM XHR unit/E2E 覆盖 setup/connect failure、abort、error/timeout/loadend settle;Server regression 覆盖 caller disconnect 后不发送 late response
Agent / DOM conversation/task/OPFS/DOM monitor 单元测试覆盖 canonical owner、attachment reachability、task scope/CAS 与同 tab monitor serialization

@cyfung1031
cyfung1031 marked this pull request as draft September 16, 2026 23:03
@cyfung1031 cyfung1031 changed the title 🔒 全面加固跨世界 GM RPC、用户脚本与 Agent 边界 🔒 全面加固跨世界 GM RPC、用户脚本、Sandbox 与 Agent 边界 Sep 29, 2026
@cyfung1031
cyfung1031 marked this pull request as ready for review October 3, 2026 18:32

@CodFrm CodFrm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有冲突需要解决一下

Comment thread vitest.config.ts
Comment on lines +48 to +49
SC_RANDOM_ESKEY: "305a6a49-2e72-48ae-bc6e-3e9517322d9b",
SC_RANDOM_FNKEY: "843078d2-403b-4ec0-a6e0-358488e135ec",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

怎么需要这么多random key

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

用途不一样。安全一点(多重防护)。 改同一个也可以

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

有冲突需要解决一下

好。我处理一下

Merge origin/main (d8c1d79) into codex/main-world-security-refactor (053cc15).

Conflict resolutions:
- content/utils.ts trimScriptInfo: keep the allowlist projection from scriptscat#1766 (pickPageLoadScriptFields) as the base. Pass the PR's derived and cloned fields as overrides: scriptRevision, metadata, value and config. The page-side isPageScriptInfo requires scriptRevision. Drop the PR's explicit execution-token deletes, because the allowlist already excludes executionHandle, executionEnvTag and executionRunFlag. pageLoad attaches those after authentication.
- content/page_load_contract.ts: extend the override type. Skip undefined overrides so an unset config stays absent, matching the own-key copy of the allowlist.
- service_worker/runtime.ts pageLoad: keep the PR's ?? -1 default, because userScriptBootstraps stores tabId as a number. Gate the popup run-count emit on tabId > 0, which is the scriptscat#1783 behavior.
- Tests: keep both sides' cases. The scriptscat#1766 exact-key producer test now also expects scriptRevision.

Verification:
- pnpm run typecheck, pnpm run lint
- pnpm test -- --run: 389 files passed
- pnpm run test:ci: one run had a timing failure in src/pages/install/useInstallData.test.ts (a file identical to main). A rerun passed all 389 files.
- pnpm run test:runtime-smoke: passed
- pnpm run test:e2e: 69 passed

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

冲突已解决

@CodFrm

CodFrm commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

复现出几个非常严重不能接受的问题:SW回收和更新脚本后,不刷新页面,脚本就无法正常使用了,会报错:"page execution binding is invalid"

这个PR已经不是人力可以弄的了,太头疼了


Review:真实浏览器验证发现的问题

验证基于当前 head 596e2506,与 main d8c1d79d 构建出的扩展做对照:用同一套脚本在 Chromium 里分别跑,表中的「base 正常 / PR 异常」都是实测结果。🔴 标注的是合并前必须修复的严重问题。CI 全绿,是因为现有 E2E 没有覆盖 SW 重启、脚本更新后旧页面继续调用 GM、大对象存储这几种情况。

🔴 1. SW 被回收后,GM 和脚本注入出问题(阻塞)

复现时直接启动 Chrome,不在 SW 上挂调试器,让它自然空闲回收(约 31–33 秒):

SW 回收后 base PR
已打开标签页里调用 GM(任何脚本) 正常 失败
新开标签页,脚本在本次浏览器启动前就已安装 正常 正常
新开标签页,脚本是本次会话内新装或更新的 正常 脚本不运行,直到浏览器重启

复现步骤:

  1. 关闭「保持后台运行」,也不要打开 SW 的 DevTools。
  2. 新装或更新一个脚本,打开匹配页面,确认脚本在运行。
  3. 什么都不操作,等到 chrome://serviceworker-internals 显示 SW 为 STOPPED(也可以直接点 Stop)。
  4. 在原页面里调用 GM,会失败;新开一个匹配页面,新装的脚本不会运行。

这里其实是两个独立的问题。

1a. scriptRevision 的值取决于对象键的顺序

getCompiledScriptRevision 对 JSON.stringify({compiledCode, metadata, …}) 求 SHA-256,early-start 的编译产物里还嵌入了 JSON.stringify(scriptInfo)。

问题在于键的顺序不固定:从 chrome.storage 读回的对象,键按字母序排列;安装或更新时内存里的对象,键保持解析时的插入顺序。实测同一个脚本,从内存缓存取出时 metadata 的键是 name, namespace, version, match, grant,从 storage 原样读出时是 grant, match, name, namespace, version,两者算出的 revision 不同(39086a1c… 对比 08100369…)。

SW 重启后 pageLoadCaches 是空的,runtime.ts:2509-2511 会用重新算出的 revision 去和存储里的比较,对不上就把这个脚本丢掉,于是 getScriptsForTab 返回 null,页面拿到 {ok:false}。冷启动时会用 storage 里的对象重建一遍,所以浏览器重启后又能恢复。

修复:在 buildCompiledResourceFromScript 入口处先把 metadata 规范化。我在本地按下面的改法验证过,SW 自然回收后,普通脚本和 early-start 脚本在新标签页都能正常运行;runtime、utils、gm_api 等 6 个相关测试文件共 329 个用例通过。

const sortKeys = <T extends Record<string, any> | undefined>(m: T): T =>
  m && (Object.fromEntries(Object.keys(m).sort().map((k) => [k, m[k]])) as T);
script = { ...script, metadata: sortKeys(script.metadata), selfMetadata: sortKeys(script.selfMetadata) };

更彻底的做法是:revision 只对规范化后的源输入求哈希(代码、metadata、匹配规则、资源,加上编译器版本),不再对编译产物求哈希。同时建议补一个单测,断言键顺序不同的同一个脚本得到相同的 revision。

1b. execution binding 只保存在 SW 内存里

pageExecutionBindings 只存在于内存中(runtime.ts:202),没有持久化,也没有重新签发的路径。所以 SW 一旦重启,所有已打开页面的 GM 调用都会报 page execution binding is invalid。

建议把 handle 改成签名令牌:会话密钥放在 chrome.storage.session(SW 重启后仍在),handle 用 HMAC 签名 {tabId, frameId, documentId, uuid, envTag, grants}。内存里的 Map 降级为缓存,查不到时验签,再根据真实 sender 和脚本当前状态重建 binding。代价是 SW 重启后请求序号窗口会被重置。

🔴 2. 脚本更新后,已打开页面的 GM 调用全部失败(阻塞)

先打开页面让脚本执行,然后更新这个脚本,再在页面里调用 GM,会报 page execution binding is invalid;base 下旧实例可以继续工作。原因是 installScript 会调用 revokePageBindingsForScript。另外从代码看,启用开关、黑名单修改和全局开关也会撤销 binding(runtime.ts:1313/1615),这几条没有实测。

建议采用 1b 的签名令牌方案后不再整体撤销,策略改为:

  • 脚本被删除或禁用:旧实例失效。
  • 脚本被更新:旧实例继续可用,权限取令牌里的旧 grants 与新 grants 的交集。

这样既保留了 base 的使用体验,也满足「旧 wrapper 不能获得新权限」的目标。

🔴 3. 大对象 GM 读写的性能严重退化(阻塞)

在浏览器里实测,对象有 2 万条记录:

操作 base PR
GM_getValue 同步读 20 次 约 48–53ms 约 3130–3240ms
await GM.setValue 写 10 次 约 1.8–2.3s 约 10.4–17.1s

原因是 customClone(global.ts:114-179)对每个对象节点,都会先分别调用 Map.prototype.forEach 和 Set.prototype.forEach 做试探,每个节点因此抛出并捕获两次异常。GM_getValue 和 valueUpdate(新旧值各一次)都会经过这里。

修复:普通对象或数组不可能是 Map/Set,可以直接跳过这两次探测:

const proto = Native.objectGetPrototypeOf(value);
const plain = proto === Object.prototype || proto === null || Native.arrayIsArray(value);
if (!plain) try { /* Map 探测 */ } catch {}
if (!plain) try { /* Set 探测 */ } catch {}

在同一个进程里对比,复制 2 万条记录的对象:base 7–25ms,PR 356–534ms,改后 15–22ms。小对象从 28–46µs 降到 2µs。改后的输出与 PR 版完全一致,带 getter 的对象仍然会被拒绝。

更进一步:GM_getValue 读的是扩展自己存储的、已经规范化过的数据,可以直接用 structuredClone,不需要再做 data-only 校验。

🔴 4. NaN/±Infinity 既存不住,还会让整批值变更被丢弃(阻塞)

  • 执行 GM_setValues({a: Infinity, b: 1}) 后,所有标签页(包括写入方自己)都不会触发 b 的监听器。原因是接收端的 isEncodedValue(script_runtime.ts:105)只认得 RType 0/1/2,不认识新加的 3/4/5,于是整个 update 被丢弃。
  • 更根本的问题是 chrome.storage.local 存不下这些值。直接写入 {a: Infinity, b: NaN},读回来这两个 key 都不见了。脚本里调用 GM_setValue("inf", Infinity) 后,ValueDAO 落盘的数据里也没有这个 key。所以 PR 描述里的「无损往返」只在 SW 内存里成立,SW 重启、浏览器重启、备份、同步之后都会丢失。现有 E2E 只刷新页面,不会重启 SW,所以测不出来。

建议在存储规范化阶段把非有限数按 JSON 语义转成 null,并删除 RType 3/4/5。

5. Date 被存成 {}(行为变更,需要确认)

GM_setValue("d", new Date(0)) 之后,PR 当场读回和刷新后读回都是 {};base 是 "1970-01-01T00:00:00.000Z"。测试里按 Tampermonkey 的行为固定成了 {},但这会破坏现有依赖 base 和 Violentmonkey 行为的脚本。建议对带 toJSON 的对象调用它。

6. 不属于任何标签页的 frame 不再注入脚本(静态分析)

issuePageBinding(runtime.ts:600)在没有 tab 时会直接抛错,导致整个 pageLoad 失败。这与 #1783「脚本照常下发」的行为冲突,runtime.ts:2145 那条注释也因此不再成立。建议没有 tab 时用 documentId 作为绑定键。这种场景在自动化环境里构造不出来,所以没有实测。

7. XHR 出错时 onloadend 的字段被改坏

请求出错后,onloadend 收到的是 readyState 0,finalUrl 和 headers 为空;base 是 readyState 4,finalUrl 也在。原因是合成的 loadend(gm_xhr.ts:582-591)先于 broker 的真实 loadend 发出,后者随即被丢掉。建议合成事件沿用 onerror 那次的响应字段,或者只在连接关闭、且始终没收到 broker 的 loadend 时才补发。

其他

  • 体积和启动延迟:content.js 和 inject.js 各增大约 36%,scripting.js 增大约 60%。10 个 document-start 脚本中最晚的启动时间从约 47ms 增加到约 53ms。
  • content 世界的日志会丢失:content.ts:30 通过 ExtensionMessage 发送 scripting/logger,SW 不处理这个前缀。
  • 只做了静态分析、未实测的点:BFCache 后退恢复会丢失 binding;Firefox 没有 documentId 时按 URL 校验,pushState 之后可能失败;iframe 频繁替换时 SW 内的状态会持续累积。
  • 范围:这个 PR 包含 248 个 commit、150 个文件,混入了 vitest 基础设施修复、与主题无关的文档改动,以及死代码(WindowMessage 类、node-worker-threads 回退分支、拼错文件名的 mesasge-channel.ts)。AGENTS.md 里仍写着 Offscreen ↔ Sandbox 走 WindowMessage,PR 描述里的 head(cc195781)也已过期。建议拆分:XHR 收尾、值同步协议、MAIN 世界传输、Agent owner 校验各自独立成 PR,并各自补上 SW 重启和脚本更新场景的 E2E。

@cyfung1031

Copy link
Copy Markdown
Collaborator Author

...看來還有一段路要走
角度不同,關注內容也不一樣
要每個角度都看清楚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants